commonlibsse_ng\re\n/
NiMath.rs

1//! NiMath module with math utility functions equivalent to NiMath.h and NiMath.cpp in C++.
2//!
3//! Provides functions for angle conversions, trigonometry, and fast arctangent calculation.
4
5/// Infinity constant, equivalent to `FLT_MAX`.
6pub const NI_INFINITY: f32 = f32::MAX;
7
8/// π
9pub const NI_PI: f32 = core::f32::consts::PI;
10
11/// π/2
12pub const NI_HALF_PI: f32 = core::f32::consts::FRAC_PI_2;
13
14/// 2π
15pub const NI_TWO_PI: f32 = core::f32::consts::TAU;
16#[allow(clippy::float_cmp_const)]
17const _: () = debug_assert!(NI_TWO_PI == 2.0 * NI_PI);
18
19/// Converts degrees to radians.
20///
21/// # Examples
22/// ```
23/// # use commonlibsse_ng::re::NiMath::deg_to_rad;
24/// assert_eq!(deg_to_rad(180.0), core::f32::consts::PI);
25/// ```
26#[inline]
27pub const fn deg_to_rad(degrees: f32) -> f32 {
28    degrees * (NI_PI / 180.0)
29}
30
31/// Converts radians to degrees.
32///
33/// # Examples
34/// ```
35/// # use commonlibsse_ng::re::NiMath::rad_to_deg;
36/// assert_eq!(rad_to_deg(core::f32::consts::PI), 180.0);
37/// assert_eq!(rad_to_deg(core::f32::consts::PI * 2.0), 360.0);
38/// ```
39#[inline]
40pub const fn rad_to_deg(radians: f32) -> f32 {
41    radians * (180.0 / NI_PI)
42}
43
44/// Normalizes an angle in radians to the range `[-π, π]`.
45///
46/// - Test with C++: [See Compiler Explorer](https://godbolt.org/z/j56qT1KP5)
47///
48/// # Examples
49/// ```
50/// // Note: 450° causes a rounding error, but 540° does not, so there is no problem using `assert`.
51/// # use commonlibsse_ng::re::NiMath::{normalize_angle, rad_to_deg};
52/// use core::f32::consts::{TAU, PI};
53/// const _: () = assert!(TAU == 2.0 * PI);
54///
55/// const RAD_OF_540DEG: f32 = TAU + PI;
56/// assert!(rad_to_deg(RAD_OF_540DEG) == 540.0);
57/// assert!(normalize_angle(RAD_OF_540DEG) == -PI);
58/// ```
59#[inline]
60pub const fn normalize_angle(radians: f32) -> f32 {
61    use core::f32::consts::{PI, TAU};
62
63    // Expand `(radians + PI).rem_euclid(TAU) - PI` for compile time evaluation.
64    let r = (radians + PI) % TAU;
65    (if r < 0.0 { r + TAU.abs() } else { r }) - PI
66}
67
68/// Returns the absolute value of a float.
69///
70/// # Examples
71/// ```
72/// # use commonlibsse_ng::re::NiMath::ni_abs;
73/// const _: () = assert!(ni_abs(-3.5) == 3.5);
74/// const _: () = assert!(ni_abs(2.0) == 2.0);
75/// ```
76#[inline]
77pub const fn ni_abs(value: f32) -> f32 {
78    value.abs()
79}
80
81/// Computes the arcsine(tilt to angle) of a value with clamping to `[-1, 1]`.
82///
83/// - Special cases:
84///     - `value >= 1.0` -> returns `π/2`
85///     - `value <= -1.0` -> returns `-π/2`
86///
87/// # Unspecified precision
88///
89/// Note that the precision for -1.0..1.0 depends on the Rust version and OS, due to the use of `f32::asin`.
90///
91/// # Examples
92///
93/// ```
94/// # use commonlibsse_ng::re::NiMath::ni_asin;
95/// use core::f32::consts::FRAC_PI_2;
96/// assert_eq!(ni_asin(FRAC_PI_2.sin()), FRAC_PI_2);
97/// assert_eq!(ni_asin(1.0), FRAC_PI_2);   // overflow -> `π/2`
98/// assert_eq!(ni_asin(-1.1), -FRAC_PI_2); // underflow -> `-π/2`
99/// ```
100#[inline]
101pub fn ni_asin(tilt: f32) -> f32 {
102    match tilt {
103        v if (-1.0..1.0).contains(&v) => v.asin(),
104        v if v >= 1.0 => NI_HALF_PI,
105        _ => -NI_HALF_PI,
106    }
107}
108
109/// Approximates the arctangent of `y/x` using a fast polynomial expansion.
110///
111/// - Special cases:
112///     - `atan2(0, 0)` → `0.0` rad
113///     - `atan2(1, 0)` → `π/2` rad
114///     - `atan2(0, -1)` → `π` rad
115///
116/// # Examples
117///
118/// ```
119/// # use commonlibsse_ng::re::NiMath::ni_fast_atan2;
120/// // `atan2(1, 0)` -> `π/2`
121/// assert!((ni_fast_atan2(1.0, 0.0) - core::f32::consts::FRAC_PI_2).abs() < 1e-6);
122/// // `atan2(0, -1)` -> `π`
123/// assert!((ni_fast_atan2(0.0, -1.0) - core::f32::consts::PI).abs() < 1e-6);
124/// ```
125///
126/// # Explanation:
127/// This function uses a polynomial approximation of `atan(z)`:
128///
129/// ```txt
130/// atan(z) ≈ z × (0.9998660 + z² × (-0.3302995 + z² × (0.1801410 +
131///            z² × (-0.0851330 + z² × 0.0208351))))
132/// ```
133///
134/// The nested form ([`Horner's method`]) is used for **faster computation**:
135/// - Reduces the number of multiplication operations.
136/// - Avoids explicit power calculations (e.g., `z^3`, `z^5`) by reusing `z²`.
137///
138/// [`Horner's method`]: https://en.wikipedia.org/wiki/Horner%27s_method
139#[inline]
140pub const fn ni_fast_atan2(y: f32, x: f32) -> f32 {
141    if x == 0.0 && y == 0.0 {
142        return 0.0;
143    }
144
145    let mut offset = 0.0;
146    let z;
147
148    if ni_abs(y) > ni_abs(x) {
149        z = x / y;
150        if z > 0.0 {
151            offset = NI_HALF_PI;
152        } else if z < 0.0 {
153            offset = -NI_HALF_PI;
154        } else {
155            return if y > 0.0 { NI_HALF_PI } else { -NI_HALF_PI };
156        }
157    } else {
158        z = y / x;
159        if z == 0.0 {
160            return if x > 0.0 { 0.0 } else { NI_PI };
161        }
162    }
163
164    let z2 = z * z;
165
166    // 5th-degree polynomial expansion for atan(z)
167    let mut result = 0.0208351; // z^5 coefficient
168    result *= z2; // z^2
169    result -= 0.0851330; // z^4 coefficient
170    result *= z2; // z^4
171    result += 0.180141; // z^3 coefficient
172    result *= z2; // z^6
173    result -= 0.3302995; // z^2 coefficient
174    result *= z2; // z^8
175    result += 0.999866; // z^1 coefficient
176    result *= z; // Multiply by z
177
178    result = offset - result;
179
180    // Adjust the result for quadrant corrections
181    if y < 0.0 && x < 0.0 {
182        result -= NI_PI; // 3rd quadrant correction
183    }
184    if y > 0.0 && x < 0.0 {
185        result += NI_PI; // 2nd quadrant correction
186    }
187
188    result
189}
190
191/// Options for configuring the floating-point comparison behavior.
192#[derive(Debug, Clone, Copy)]
193pub struct ComparisonOptions {
194    /// The relative tolerance used for comparison (epsilon).
195    /// This is typically a small value like `FLT_EPSILON`.
196    pub epsilon: f32,
197
198    /// The absolute tolerance used for comparison.
199    /// This is typically used for very small numbers.
200    pub abs_th: f32,
201}
202
203impl ComparisonOptions {
204    /// Creates a new `ComparisonOptions` instance with default values.
205    ///
206    /// The default values are `epsilon = 128 * FLT_EPSILON` and `abs_th = FLT_MIN`.
207    #[inline]
208    pub const fn new(epsilon: f32, abs_th: f32) -> Self {
209        Self { epsilon, abs_th }
210    }
211
212    /// Returns default comparison options with typical values:
213    /// - `epsilon`: 128 * FLT_EPSILON
214    /// - `abs_th`: FLT_MIN
215    #[inline]
216    pub const fn const_default() -> Self {
217        Self { epsilon: 128.0 * f32::EPSILON, abs_th: f32::MIN }
218    }
219}
220
221impl Default for ComparisonOptions {
222    #[inline]
223    fn default() -> Self {
224        Self { epsilon: 128.0 * f32::EPSILON, abs_th: f32::MIN }
225    }
226}
227
228/// Compares two floating point numbers `a` and `b` to see if they are nearly equal.
229///
230/// This function uses both relative and absolute comparisons to handle cases where the values
231/// differ by a small but significant amount due to floating-point precision limitations.
232///
233/// - origin: https://stackoverflow.com/questions/4915462/how-should-i-do-floating-point-comparison
234///
235/// # Panics
236/// Ensure that `epsilon` is within a reasonable range:
237/// - It should not be smaller than the smallest representable float (`FLT_EPSILON`)
238/// - It should not be equal to or greater than 1.0 to avoid overly lenient comparisons
239#[inline]
240pub const fn nearly_equal(a: f32, b: f32, options: ComparisonOptions) -> bool {
241    assert!(f32::EPSILON <= options.epsilon, "epsilon must be >= FLT_EPSILON");
242    assert!(options.epsilon < 1.0, "epsilon must be < 1.0");
243
244    #[allow(clippy::float_cmp)]
245    if a == b {
246        return true; // No need to do further comparison if they are exactly equal
247    }
248
249    let diff = (a - b).abs();
250    let norm = (a + b).abs().min(f32::MAX);
251    diff < f32::max(options.abs_th, options.epsilon * norm)
252}
253
254#[cfg(test)]
255mod tests {
256    #![allow(clippy::float_cmp)]
257    #![allow(clippy::float_cmp_const)]
258    #![allow(clippy::missing_const_for_fn)]
259    use super::*;
260    use crate::assert_nearly_eq;
261    use core::f32::consts::{FRAC_PI_2, PI, TAU};
262
263    #[test]
264    const fn test_deg_to_rad() {
265        assert!(deg_to_rad(180.0) == PI);
266        assert!(deg_to_rad(90.0) == FRAC_PI_2);
267    }
268
269    #[test]
270    const fn test_rad_to_deg() {
271        assert!(rad_to_deg(PI) == 180.0);
272        assert!(rad_to_deg(FRAC_PI_2) == 90.0);
273    }
274
275    #[test]
276    fn test_normalize_angle() {
277        // 450 - 360 = 90 degrees
278        const _: () = {
279            const RAD_OF_90DEG: f32 = FRAC_PI_2;
280            assert!(rad_to_deg(RAD_OF_90DEG) == 90.0);
281
282            const RAD_OF_450DEG: f32 = (5.0 * PI) / 2.0;
283            assert!(rad_to_deg(RAD_OF_450DEG) == 450.0);
284
285            const EPSILON: f32 = 4.0 * 1e-7;
286            assert!(EPSILON == 0.0000004);
287
288            assert!(normalize_angle(RAD_OF_450DEG) == RAD_OF_90DEG + EPSILON);
289        };
290
291        const _: () = {
292            const RAD_OF_540DEG: f32 = TAU + PI;
293            assert!(TAU == 2.0 * PI);
294            assert!(rad_to_deg(RAD_OF_540DEG) == 540.0);
295            assert!(normalize_angle(RAD_OF_540DEG) == -PI);
296        };
297    }
298
299    #[test]
300    fn test_ni_abs() {
301        assert!(ni_abs(-5.0) == 5.0);
302        assert!(ni_abs(5.0) == 5.0);
303    }
304
305    #[test]
306    fn test_ni_asin() {
307        assert_eq!(ni_asin(1.0), FRAC_PI_2);
308        assert_eq!(ni_asin(-1.0), -FRAC_PI_2);
309        assert_eq!(ni_asin(0.0), 0.0);
310    }
311
312    #[test]
313    fn test_ni_fast_atan2() {
314        assert_nearly_eq!(ni_fast_atan2(1.0, 0.0), FRAC_PI_2);
315        assert_nearly_eq!(ni_fast_atan2(0.0, -1.0), PI);
316    }
317}